﻿// Copyright (c) MichaConrad. All rights reserved. See License.txt in the project root for license information.

namespace LinqToLdapExtensions
{
    using System;
    using System.DirectoryServices.Protocols;
    using LinqToLdap;

    using LinqToLdapExtensions.Properties;
    using LinqToLdapExtensions.Scope;

    /// <summary>
    /// <para>The directory context factory is responsible for generating <see cref="LinqToLdap.DirectoryContext"/> instances.
    /// </para>
    /// <para>The basic usage pattern for the factory:
    /// <list type="bullet">
    /// <item><description><see cref="IDirectoryContextFactory.CreateContext()"/> to factor a new <see cref="IDirectoryContext"/></description></item>
    /// <item><description>Bind() the context to <see cref="IDirectoryContextFactory.CurrentDirectoryContextBinding"/></description></item>
    /// <item><description>prior to be able to call <see cref="IDirectoryContextFactory.GetCurrentContext()"/></description></item>
    /// </list>
    /// </para>
    /// </summary>
    /// <example>
    /// The following is the basic usage pattern example for the factory.
    /// <code>
    /// <![CDATA[
    ///     // instantiate the factory with the scoping you need.
    ///     var factory = new DirectoryContextFactory(ScopeState.ThreadStatic);
    /// 
    ///     // create a new directory context instance.
    ///     var ctx = factory.CreateContext();
    /// 
    ///     // bind it to the scope.
    ///     factory.CurrentDirectoryContextBinding.Bind(ctx);
    ///     // or 
    ///     // CurrentDirectoryContext.Bind(factory, ctx);
    /// 
    ///     // now we can use GetCurrentContext whenever needed.
    ///     var current = factory.GetCurrentContext();
    /// ]]>
    /// </code>
    /// </example>
    /// <seealso cref="ScopeState"/>
    /// <seealso cref="DirectoryContextUnitOfWork"/>
    public class DirectoryContextFactory : IDirectoryContextFactory
    {
        /// <summary>
        /// The connection.
        /// This might be set by the constructor and used by <see cref="IDirectoryContextFactory.CreateContext()"/>
        /// </summary>
        private readonly LdapConnection connection;

        /// <summary>
        /// The dispose of connection flag.
        /// This might be set by the constructor and used by <see cref="IDirectoryContextFactory.CreateContext()"/>
        /// </summary>
        private readonly bool disposeOfConnection;

        /// <summary>
        /// The configuration.
        /// This might be set by the constructor and used by <see cref="IDirectoryContextFactory.CreateContext()"/>
        /// </summary>
        private readonly ILdapConfiguration configuration;

        /// <summary>
        /// The test key.
        /// </summary>
        private readonly Guid key;
        
        /// <summary>
        /// <para>Initializes a new instance of the <see cref="DirectoryContextFactory"/> class,
        /// sets <see cref="DirectoryContextFactory.CurrentDirectoryContextBinding"/> based on the given scope state
        /// and will use the corresponding constructor of
        /// <see cref="LinqToLdap.DirectoryContext" /> whenever <see cref="DirectoryContextFactory.CreateContext()"/> gets called
        /// </para>
        /// <para>In this case <see cref="DirectoryContextFactory.CreateContext()"/> will create an instance for querying. 
        /// The underlying connection will be disposed of when the instance of <see cref="LinqToLdap.DirectoryContext" /> is disposed
        /// if the <see cref="LinqToLdap.LdapConfiguration"/> does not have a configured <see cref="LinqToLdap.ILdapConnectionFactory"/>. 
        /// </para>
        /// <para>Otherwise <see cref="LinqToLdap.ILdapConnectionFactory.ReleaseConnection(System.DirectoryServices.Protocols.LdapConnection)"/>
        /// will be called.
        /// </para>
        /// </summary>
        /// <param name="scopeState">The type of scope to use.</param>
        /// <seealso cref="LinqToLdap.DirectoryContext"/> 
        public DirectoryContextFactory(ScopeState scopeState)
        {
            this.key = Guid.NewGuid();
            this.CurrentDirectoryContextBinding = this.BuildCurrentDirectoryContext(scopeState);
        }

        /// <summary>
        /// <para>Initializes a new instance of the <see cref="DirectoryContextFactory"/> class,
        /// sets <see cref="DirectoryContextFactory.CurrentDirectoryContextBinding"/> based on the given scope state
        /// and will use the corresponding constructor of
        /// <see cref="LinqToLdap.DirectoryContext" /> whenever <see cref="DirectoryContextFactory.CreateContext()"/> gets called
        /// </para>
        /// <para>In this case <see cref="DirectoryContextFactory.CreateContext()"/> will 
        /// create an instance for querying.  The underlying connection will be disposed
        /// of when the instance of <see cref="LinqToLdap.DirectoryContext" /> is disposed if the <see cref="LinqToLdap.LdapConfiguration"/> does
        /// not have a configured <see cref="LinqToLdap.ILdapConnectionFactory"/>.</para>
        /// <para>Otherwise <see cref="LinqToLdap.ILdapConnectionFactory.ReleaseConnection(System.DirectoryServices.Protocols.LdapConnection)"/>
        /// will be called.</para>
        /// </summary>
        /// <param name="scopeState">The type of scope to use.</param>
        /// <param name="configuration">The configuration.</param>
        public DirectoryContextFactory(ScopeState scopeState, ILdapConfiguration configuration)
            : this(scopeState)
        {
            this.configuration = configuration;
        }

        /// <summary>
        /// <para>Initializes a new instance of the <see cref="DirectoryContextFactory"/> class,
        /// sets <see cref="DirectoryContextFactory.CurrentDirectoryContextBinding"/> based on the given scope state
        /// and will use the corresponding constructor of
        /// <see cref="LinqToLdap.DirectoryContext" /> whenever <see cref="DirectoryContextFactory.CreateContext()"/> gets called
        /// </para>
        /// <para>In this case <see cref="DirectoryContextFactory.CreateContext()"/> will create 
        /// an instance and uses the specified connection for querying. 
        /// The connection will not be disposed of when the instance of <see cref="LinqToLdap.DirectoryContext" /> is disposed.</para>
        /// </summary>
        /// <param name="scopeState">The type of scope to use.</param>
        /// <param name="connection">The connection.</param>
        /// <param name="disposeOfConnection">If set to <c>true</c> the underlying connection will be disposed.</param>
        /// <param name="configuration">The configuration.</param>
        public DirectoryContextFactory(ScopeState scopeState, LdapConnection connection, bool disposeOfConnection = false, ILdapConfiguration configuration = null)
            : this(scopeState)
        {
            this.connection = connection;
            this.disposeOfConnection = disposeOfConnection;
            this.configuration = configuration;
        }
        
        /// <summary>
        /// Gets the current directory context.
        /// </summary>
        public ICurrentDirectoryContextBinding CurrentDirectoryContextBinding { get; private set; }

        /// <summary>
        /// Gets the key to do some testing.
        /// TODO: remove this, it's just for debugging (finding out if the same factory is used in some places...)
        /// </summary>
        public Guid Key
        {
            get
            {
                return this.key;
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="LinqToLdap.DirectoryContext" /> 
        /// based on the configuration options given through initialization of this factory.
        /// </summary>
        /// <returns>A new instance of <see cref="LinqToLdap.DirectoryContext" />.</returns>
        /// <seealso cref="DirectoryContextFactory(ScopeState)"/>
        /// <seealso cref="DirectoryContextFactory(ScopeState, ILdapConfiguration)"/>
        /// <seealso cref="DirectoryContextFactory(ScopeState, LdapConnection, bool, ILdapConfiguration)"/>
        public IDirectoryContext CreateContext()
        {
            IDirectoryContext ctx;
            if (this.connection != null)
            {
                ctx = new DirectoryContext(this.connection, this.disposeOfConnection, this.configuration);
            }
            else if (this.configuration != null)
            {
                ctx = new DirectoryContext(this.configuration);
            }
            else
            {
                ctx = new DirectoryContext();
            }

            return ctx;
        }

        /// <summary>
        /// <para>Gets the current <see cref="IDirectoryContext"/> bound to the scope.</para>
        /// <para>This method does not create a new instance. Call <see cref="IDirectoryContextFactory.CreateContext()"/> first 
        /// and bind it to <see cref="IDirectoryContextFactory.CurrentDirectoryContextBinding"/>.
        /// </para>
        /// </summary>
        /// <returns>
        /// The <see cref="IDirectoryContext"/> instance in the current scope.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// If no binding is defined.
        /// </exception>
        public IDirectoryContext GetCurrentContext()
        {
            if (!this.CurrentDirectoryContextBinding.HasBind())
            {
                throw new InvalidOperationException(Resources.NoContextBoundToCurrentScope);
            }

            return this.CurrentDirectoryContextBinding.CurrentContext;
        }

        /// <summary>
        /// Builds current directory context.
        /// </summary>
        /// <param name="scopeState">
        /// The scope state.
        /// </param>
        /// <returns>
        /// The <see cref="ICurrentDirectoryContext"/>.
        /// </returns>
        private ICurrentDirectoryContextBinding BuildCurrentDirectoryContext(ScopeState scopeState)
        {
            switch (scopeState)
            {
                case ScopeState.ThreadStatic:
                    return new ThreadStaticDirectoryContext(this);
                case ScopeState.WebContext:
                    return new WebDirectoryContext(this);
            }

            return null;
        }
    }
}
